/*
 * Created on 15-Nov-2004
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package org.gudy.azureus2.core3.download.impl;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.gudy.azureus2.core3.category.Category;
import org.gudy.azureus2.core3.category.CategoryManager;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.disk.DiskManagerFactory;
import org.gudy.azureus2.core3.disk.DiskManagerFileInfo;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerException;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.download.DownloadManagerStateAttributeListener;
import org.gudy.azureus2.core3.download.DownloadManagerStateEvent;
import org.gudy.azureus2.core3.download.DownloadManagerStateListener;
import org.gudy.azureus2.core3.ipfilter.IpFilterManagerFactory;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.LogRelation;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.peer.PEPeerSource;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentAnnounceURLGroup;
import org.gudy.azureus2.core3.torrent.TOTorrentAnnounceURLSet;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.torrent.TOTorrentFile;
import org.gudy.azureus2.core3.torrent.TOTorrentListener;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncer;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AENetworkClassifier;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.StringInterner;
import org.gudy.azureus2.core3.util.TorrentUtils;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.CopyOnWriteMap;
import com.aelitis.azureus.core.util.LinkFileMap;

/**
 * @author parg Overall aim of this is to stop updating the torrent file itself and update something Azureus owns. To this end a file based on torrent
 *         hash is created in user-dir/active It is actually just a copy of the torrent file
 */

public class DownloadManagerStateImpl implements DownloadManagerState, ParameterListener {
    private static final int VER_INCOMING_PEER_SOURCE = 1;
    private static final int VER_CURRENT = VER_INCOMING_PEER_SOURCE;

    private static final LogIDs LOGID = LogIDs.DISK;
    private static final String RESUME_KEY = "resume";
    private static final String TRACKER_CACHE_KEY = "tracker_cache";
    private static final String ATTRIBUTE_KEY = "attributes";
    private static final String AZUREUS_PROPERTIES_KEY = "azureus_properties";

    private static final File ACTIVE_DIR;

    public static boolean SUPPRESS_FIXUP_ERRORS = false;

    static {

        ACTIVE_DIR = FileUtil.getUserFile("active");

        if (!ACTIVE_DIR.exists()) {

            FileUtil.mkdirs(ACTIVE_DIR);
        }
    }

    private static Random random = RandomUtils.SECURE_RANDOM;

    private static final Map default_parameters;
    private static final Map default_attributes;

    static {
        default_parameters = new HashMap();

        for (int i = 0; i < PARAMETERS.length; i++) {

            default_parameters.put(PARAMETERS[i][0], PARAMETERS[i][1]);
        }

        default_attributes = new HashMap();

        for (int i = 0; i < ATTRIBUTE_DEFAULTS.length; i++) {

            default_attributes.put(ATTRIBUTE_DEFAULTS[i][0], ATTRIBUTE_DEFAULTS[i][1]);
        }

        // only add keys that will point to Map objects here!
        TorrentUtils.registerMapFluff(new String[] { TRACKER_CACHE_KEY, RESUME_KEY });
    }

    private static AEMonitor class_mon = new AEMonitor("DownloadManagerState:class");

    private static Map<HashWrapper, DownloadManagerStateImpl> state_map = new HashMap<HashWrapper, DownloadManagerStateImpl>();

    static {
        ParameterListener listener = new ParameterListener() {
            public void parameterChanged(String parameterName) {
                List<DownloadManagerStateImpl> states;

                synchronized (state_map) {

                    states = new ArrayList<DownloadManagerStateImpl>(state_map.values());
                }

                for (DownloadManagerStateImpl state : states) {

                    try {
                        state.parameterChanged(parameterName);

                    } catch (Throwable e) {

                        Debug.out(e);
                    }
                }
            }
        };

        COConfigurationManager.addParameterListener("Max.Peer.Connections.Per.Torrent.When.Seeding", listener);
        COConfigurationManager.addParameterListener("Max.Peer.Connections.Per.Torrent.When.Seeding.Enable", listener);
        COConfigurationManager.addParameterListener("Max.Peer.Connections.Per.Torrent", listener);
        COConfigurationManager.addParameterListener("Max Uploads", listener);
        COConfigurationManager.addParameterListener("Max Uploads Seeding", listener);
        COConfigurationManager.addParameterListener("Max Seeds Per Torrent", listener);
        COConfigurationManager.addParameterListener("enable.seedingonly.maxuploads", listener);
    }

    private static Map global_state_cache = new HashMap();
    private static ArrayList global_state_cache_wrappers = new ArrayList();

    private DownloadManagerImpl download_manager;

    private final TorrentUtils.ExtendedTorrent torrent;

    private boolean write_required;

    private Category category;

    private CopyOnWriteList listeners_cow = new CopyOnWriteList();

    private CopyOnWriteMap listeners_read_map_cow = new CopyOnWriteMap();
    private CopyOnWriteMap listeners_write_map_cow = new CopyOnWriteMap();

    private Map parameters;
    private Map attributes;

    private AEMonitor this_mon = new AEMonitor("DownloadManagerState");

    private int supressWrites = 0;

    private static ThreadLocal tls_wbr = new ThreadLocal() {
        public Object initialValue() {
            return (new ArrayList(1));
        }
    };

    private static DownloadManagerState getDownloadState(DownloadManagerImpl download_manager, TOTorrent original_torrent,
            TorrentUtils.ExtendedTorrent target_torrent)

    throws TOTorrentException {
        byte[] hash = target_torrent.getHash();

        DownloadManagerStateImpl res = null;

        try {
            class_mon.enter();

            HashWrapper hash_wrapper = new HashWrapper(hash);

            res = (DownloadManagerStateImpl) state_map.get(hash_wrapper);

            if (res == null) {

                res = new DownloadManagerStateImpl(download_manager, target_torrent);

                state_map.put(hash_wrapper, res);

            } else {

                // if original state was created without a download manager,
                // bind it to this one

                if (res.getDownloadManager() == null && download_manager != null) {

                    res.setDownloadManager(download_manager);
                }

                if (original_torrent != null) {

                    res.mergeTorrentDetails(original_torrent);
                }
            }
        } finally {

            class_mon.exit();
        }

        return (res);
    }

    public static DownloadManagerState getDownloadState(TOTorrent original_torrent)

    throws TOTorrentException {
        byte[] torrent_hash = original_torrent.getHash();

        // System.out.println( "getDownloadState: hash = " + ByteFormatter.encodeString(torrent_hash));

        TorrentUtils.ExtendedTorrent saved_state = null;

        File saved_file = getStateFile(torrent_hash);

        if (saved_file.exists()) {

            try {
                saved_state = TorrentUtils.readDelegateFromFile(saved_file, false);

            } catch (Throwable e) {

                Debug.out("Failed to load download state for " + saved_file, e);
            }
        }

        // if saved state not found then recreate from original torrent

        if (saved_state == null) {

            TorrentUtils.copyToFile(original_torrent, saved_file);

            saved_state = TorrentUtils.readDelegateFromFile(saved_file, false);
        }

        return (getDownloadState(null, original_torrent, saved_state));
    }

    protected static DownloadManagerState getDownloadState(DownloadManagerImpl download_manager, String torrent_file, byte[] torrent_hash,
            boolean inactive)

    throws TOTorrentException {
        boolean discard_pieces = state_map.size() > 32;

        // System.out.println( "getDownloadState: hash = " + (torrent_hash==null?"null":ByteFormatter.encodeString(torrent_hash) + ", file = " +
        // torrent_file ));

        TOTorrent original_torrent = null;
        TorrentUtils.ExtendedTorrent saved_state = null;

        // first, if we already have the hash then see if we can load the saved state

        if (torrent_hash != null) {

            File saved_file = getStateFile(torrent_hash);

            if (saved_file.exists()) {

                try {
                    Map cached_state = (Map) global_state_cache.remove(new HashWrapper(torrent_hash));

                    if (cached_state != null) {

                        CachedStateWrapper wrapper = new CachedStateWrapper(download_manager, torrent_file, torrent_hash, cached_state, inactive);

                        global_state_cache_wrappers.add(wrapper);

                        saved_state = wrapper;

                    } else {

                        saved_state = TorrentUtils.readDelegateFromFile(saved_file, discard_pieces);
                    }

                } catch (Throwable e) {

                    Debug.out("Failed to load download state for " + saved_file);
                }
            }
        }

        // if saved state not found then recreate from original torrent if required

        if (saved_state == null) {

            original_torrent = TorrentUtils.readDelegateFromFile(new File(torrent_file), discard_pieces);

            torrent_hash = original_torrent.getHash();

            File saved_file = getStateFile(torrent_hash);

            if (saved_file.exists()) {

                try {
                    saved_state = TorrentUtils.readDelegateFromFile(saved_file, discard_pieces);

                } catch (Throwable e) {

                    Debug.out("Failed to load download state for " + saved_file);
                }
            }

            if (saved_state == null) {

                // we must copy the torrent as we want one independent from the
                // original (someone might still have references to the original
                // and do stuff like write it somewhere else which would screw us
                // up)

                TorrentUtils.copyToFile(original_torrent, saved_file);

                saved_state = TorrentUtils.readDelegateFromFile(saved_file, discard_pieces);
            }
        }

        DownloadManagerState res = getDownloadState(download_manager, original_torrent, saved_state);

        if (inactive) {

            res.setActive(false);
        }

        return (res);
    }

    protected static File getStateFile(byte[] torrent_hash) {
        return (new File(ACTIVE_DIR, ByteFormatter.encodeString(torrent_hash) + ".dat"));
    }

    protected static File getGlobalStateFile() {
        return (new File(ACTIVE_DIR, "cache.dat"));
    }

    public static void loadGlobalStateCache() {
        File file = getGlobalStateFile();

        if (!file.canRead()) {

            return;
        }

        try {

            BufferedInputStream is = new BufferedInputStream(new GZIPInputStream(new FileInputStream(file)));

            try {

                Map map = BDecoder.decode(is);

                List cache = (List) map.get("state");

                if (cache != null) {

                    for (int i = 0; i < cache.size(); i++) {

                        Map entry = (Map) cache.get(i);

                        byte[] hash = (byte[]) entry.get("hash");

                        if (hash != null) {

                            global_state_cache.put(new HashWrapper(hash), entry);
                        }
                    }
                }

                is.close();

            } catch (IOException e) {

                Debug.printStackTrace(e);
            } finally {

                try {
                    is.close();

                } catch (Throwable e) {
                }
            }
        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }
    }

    public static void saveGlobalStateCache() {
        try {
            class_mon.enter();

            Map map = new HashMap();

            List cache = new ArrayList();

            map.put("state", cache);

            Iterator it = state_map.values().iterator();

            while (it.hasNext()) {

                DownloadManagerState dms = (DownloadManagerState) it.next();

                DownloadManager dm = dms.getDownloadManager();

                if (dm != null && dm.isPersistent()) {

                    try {
                        Map state = CachedStateWrapper.export(dms);

                        cache.add(state);

                    } catch (Throwable e) {

                        Debug.printStackTrace(e);
                    }
                }
            }

            GZIPOutputStream os = new GZIPOutputStream(new FileOutputStream(getGlobalStateFile()));

            try {

                os.write(BEncoder.encode(map));

                os.close();

            } catch (IOException e) {

                Debug.printStackTrace(e);

                try {
                    os.close();

                } catch (IOException f) {

                }
            }
        } catch (Throwable e) {

            Debug.printStackTrace(e);

        } finally {

            class_mon.exit();

        }
    }

    public static void discardGlobalStateCache() {
        getGlobalStateFile().delete();

        for (int i = 0; i < global_state_cache_wrappers.size(); i++) {

            ((CachedStateWrapper) global_state_cache_wrappers.get(i)).clearCache();
        }

        global_state_cache_wrappers.clear();
        global_state_cache_wrappers.trimToSize();
    }

    public static void importDownloadState(File source_dir, byte[] download_hash)

    throws DownloadManagerException {
        String hash_str = ByteFormatter.encodeString(download_hash);

        String state_file = hash_str + ".dat";

        File target_state_file = new File(ACTIVE_DIR, state_file);
        File source_state_file = new File(source_dir, state_file);

        if (!source_state_file.exists()) {

            throw (new DownloadManagerException("Source state file missing: " + source_state_file));
        }

        if (target_state_file.exists()) {

            throw (new DownloadManagerException("Target state file already exists: " + target_state_file));
        }

        if (!FileUtil.copyFile(source_state_file, target_state_file)) {

            throw (new DownloadManagerException("Failed to copy state file: " + source_state_file + " -> " + target_state_file));
        }

        File source_state_dir = new File(source_dir, hash_str);

        if (source_state_dir.exists()) {

            File target_state_dir = new File(ACTIVE_DIR, hash_str);

            try {
                FileUtil.copyFileOrDirectory(source_state_dir, target_state_dir);

            } catch (Throwable e) {

                target_state_file.delete();

                throw (new DownloadManagerException("Failed to copy state dir: " + source_dir + " -> " + target_state_dir, e));
            }
        }
    }

    public static void deleteDownloadState(byte[] download_hash)

    throws DownloadManagerException {
        deleteDownloadState(ACTIVE_DIR, download_hash);
    }

    public static void deleteDownloadState(File source_dir, byte[] download_hash)

    throws DownloadManagerException {
        String hash_str = ByteFormatter.encodeString(download_hash);

        String state_file = hash_str + ".dat";

        File target_state_file = new File(source_dir, state_file);

        if (target_state_file.exists()) {

            if (!target_state_file.delete()) {

                throw (new DownloadManagerException("Failed to delete state file: " + target_state_file));
            }
        }

        File target_state_dir = new File(source_dir, hash_str);

        if (target_state_dir.exists()) {

            if (!FileUtil.recursiveDelete(target_state_dir)) {

                throw (new DownloadManagerException("Failed to delete state dir: " + target_state_dir));
            }
        }
    }

    protected DownloadManagerStateImpl(DownloadManagerImpl _download_manager, TorrentUtils.ExtendedTorrent _torrent) {
        download_manager = _download_manager;
        torrent = _torrent;

        attributes = torrent.getAdditionalMapProperty(ATTRIBUTE_KEY);

        if (attributes == null) {

            attributes = new HashMap();
        }

        String cat_string = getStringAttribute(AT_CATEGORY);

        if (cat_string != null) {

            Category cat = CategoryManager.getCategory(cat_string);

            if (cat != null) {

                setCategory(cat);
            }
        }

        parameters = getMapAttribute(AT_PARAMETERS);

        if (parameters == null) {

            parameters = new HashMap();
        }

        // note that version will be -1 for the first time through this code

        int version = getIntAttribute(AT_VERSION);

        if (version < VER_INCOMING_PEER_SOURCE) {

            // migrate by adding incoming as enabled - only needed if we have any specified as other
            // code takes care of the case where we have none

            if (getPeerSources().length > 0) {

                if (PEPeerSource.isPeerSourceEnabledByDefault(PEPeerSource.PS_INCOMING)) {

                    setPeerSourceEnabled(PEPeerSource.PS_INCOMING, true);
                }
            } else {

                // set default for newly added torrent

                setPeerSources(PEPeerSource.getDefaultEnabledPeerSources());
            }
        }

        long flags = getFlags();

        if ((flags & FLAG_DISABLE_IP_FILTER) != 0) {

            try {
                IpFilterManagerFactory.getSingleton().getIPFilter().addExcludedHash(torrent.getHash());

            } catch (Throwable e) {

                Debug.out(e);
            }
        }

        if (version < VER_CURRENT) {

            setIntAttribute(AT_VERSION, VER_CURRENT);
        }
    }

    public void parameterChanged(String parameterName) {
        // get any listeners to pick up new values as their defaults are based on core params

        informWritten(AT_PARAMETERS);
    }

    public DownloadManager getDownloadManager() {
        return (download_manager);
    }

    protected void setDownloadManager(DownloadManagerImpl dm) {
        download_manager = dm;
    }

    public File getStateFile() {
        try {
            File parent = new File(ACTIVE_DIR, ByteFormatter.encodeString(torrent.getHash()) + File.separatorChar);

            return (StringInterner.internFile(parent));

        } catch (Throwable e) {

            Debug.printStackTrace(e);

            return (null);
        }
    }

    public void clearTrackerResponseCache() {
        setTrackerResponseCache(new HashMap());
    }

    public Map getTrackerResponseCache() {

        Map tracker_response_cache = null;

        tracker_response_cache = torrent.getAdditionalMapProperty(TRACKER_CACHE_KEY);

        if (tracker_response_cache == null)
            tracker_response_cache = new HashMap();

        return (tracker_response_cache);
    }

    public void setTrackerResponseCache(Map value) {

        try {
            this_mon.enter();

            // System.out.println( "setting download state/tracker cache for '" + new String(torrent.getName()));

            boolean changed = !BEncoder.mapsAreIdentical(value, getTrackerResponseCache());

            if (changed) {

                write_required = true;

                torrent.setAdditionalMapProperty(TRACKER_CACHE_KEY, value);
            }

        } finally {

            this_mon.exit();
        }
    }

    public Map getResumeData() {
        try {
            this_mon.enter();

            return (torrent.getAdditionalMapProperty(RESUME_KEY));

        } finally {

            this_mon.exit();
        }
    }

    public void clearResumeData() {
        setResumeData(null);
    }

    public void setResumeData(Map data) {
        try {
            this_mon.enter();

            // System.out.println( "setting download state/resume data for '" + new String(torrent.getName()));

            if (data == null) {

                setLongAttribute(AT_RESUME_STATE, 1);

                torrent.removeAdditionalProperty(RESUME_KEY);

            } else {

                torrent.setAdditionalMapProperty(RESUME_KEY, data);

                boolean complete = DiskManagerFactory.isTorrentResumeDataComplete(this);

                setLongAttribute(AT_RESUME_STATE, complete ? 2 : 1);
            }

            write_required = true;

        } finally {

            this_mon.exit();
        }

        // we need to ensure this is persisted now as it has implications regarding crash restarts etc

        save();
    }

    public boolean isResumeDataComplete() {
        // this is a cache of resume state to speed up startup

        long state = getLongAttribute(AT_RESUME_STATE);

        if (state == 0) {

            // don't know

            boolean complete = DiskManagerFactory.isTorrentResumeDataComplete(this);

            setLongAttribute(AT_RESUME_STATE, complete ? 2 : 1);

            return (complete);

        } else {

            return (state == 2);
        }
    }

    public TOTorrent getTorrent() {
        return (torrent);
    }

    public void setActive(boolean active) {
        torrent.setDiscardFluff(!active);
    }

    public void discardFluff() {
        torrent.setDiscardFluff(true);
    }

    public boolean exportState(File target_dir) {
        try {
            this_mon.enter();

            save(true);

            byte[] hash = torrent.getHash();

            String hash_str = ByteFormatter.encodeString(hash);

            String state_file = hash_str + ".dat";

            File existing_state_file = new File(ACTIVE_DIR, state_file);
            File target_state_file = new File(target_dir, state_file);

            if (!FileUtil.copyFile(existing_state_file, target_state_file)) {

                throw (new IOException("Failed to copy state file"));
            }

            File existing_state_dir = new File(ACTIVE_DIR, hash_str);

            if (existing_state_dir.exists()) {

                File target_state_dir = new File(target_dir, hash_str);

                FileUtil.copyFileOrDirectory(existing_state_dir, target_state_dir);
            }

            return (true);

        } catch (Throwable e) {

            Debug.out(e);

            return (false);

        } finally {

            this_mon.exit();
        }
    }

    public void suppressStateSave(boolean suppress) {
        if (suppress)
            supressWrites++;
        else if (supressWrites > 0)
            supressWrites--;
    }

    public void save() {
        save(false);
    }

    protected void save(boolean force) {
        if (supressWrites > 0 && !force) {

            return;
        }

        boolean do_write;

        try {
            this_mon.enter();

            do_write = write_required;

            write_required = false;

        } finally {

            this_mon.exit();
        }

        if (do_write) {

            try {
                // System.out.println( "writing download state for '" + new String(torrent.getName()));

                if (Logger.isEnabled())
                    Logger.log(new LogEvent(torrent, LOGID, "Saving state for download '" + TorrentUtils.getLocalisedName(torrent) + "'"));

                torrent.setAdditionalMapProperty(ATTRIBUTE_KEY, attributes);

                TorrentUtils.writeToFile(torrent, true);

            } catch (Throwable e) {
                Logger.log(new LogEvent(torrent, LOGID, "Saving state", e));
            }
        } else {

            // System.out.println( "not writing download state for '" + new String(torrent.getName()));
        }
    }

    public void delete() {
        try {
            class_mon.enter();

            HashWrapper wrapper = torrent.getHashWrapper();

            state_map.remove(wrapper);

            TorrentUtils.delete(torrent);

            File dir = new File(ACTIVE_DIR, ByteFormatter.encodeString(wrapper.getBytes()));

            if (dir.exists() && dir.isDirectory()) {

                FileUtil.recursiveDelete(dir);
            }
        } catch (Throwable e) {

            Debug.printStackTrace(e);

        } finally {

            class_mon.exit();
        }
    }

    protected void mergeTorrentDetails(TOTorrent other_torrent) {
        try {
            boolean write = TorrentUtils.mergeAnnounceURLs(other_torrent, torrent);

            // System.out.println( "DownloadManagerState:mergeTorrentDetails -> " + write );

            if (write) {

                save();

                if (download_manager != null) {

                    TRTrackerAnnouncer client = download_manager.getTrackerClient();

                    if (client != null) {

                        // pick up any URL changes

                        client.resetTrackerUrl(false);
                    }
                }
            }
        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }
    }

    public void setFlag(long flag, boolean set) {
        long old_value = getLongAttribute(AT_FLAGS);

        long new_value;

        if (set) {

            new_value = old_value | flag;

        } else {

            new_value = old_value & ~flag;
        }

        if (old_value != new_value) {

            setLongAttribute(AT_FLAGS, new_value);

            if ((old_value & FLAG_DISABLE_IP_FILTER) != (new_value & FLAG_DISABLE_IP_FILTER)) {

                try {
                    if ((new_value & FLAG_DISABLE_IP_FILTER) != 0) {

                        IpFilterManagerFactory.getSingleton().getIPFilter().addExcludedHash(torrent.getHash());

                    } else {

                        IpFilterManagerFactory.getSingleton().getIPFilter().removeExcludedHash(torrent.getHash());

                    }
                } catch (Throwable e) {

                    Debug.out(e);
                }
            }
        }
    }

    public boolean getFlag(long flag) {
        long value = getLongAttribute(AT_FLAGS);

        return ((value & flag) != 0);
    }

    public long getFlags() {
        return (getLongAttribute(AT_FLAGS));
    }

    public boolean parameterExists(String name) {
        return parameters.containsKey(name);
    }

    public void setParameterDefault(String name) {
        try {
            this_mon.enter();

            Object value = parameters.get(name);

            if (value == null) {

                return;
            }

            // gotta clone here otherwise we update the underlying map and the setMapAttribute code
            // doesn't think it has changed

            parameters = new LightHashMap(parameters);

            parameters.remove(name);

        } finally {

            this_mon.exit();
        }

        setMapAttribute(AT_PARAMETERS, parameters);
    }

    public long getLongParameter(String name) {
        try {
            this_mon.enter();

            Object value = parameters.get(name);

            if (value == null) {

                value = default_parameters.get(name);

                if (value == null) {

                    Debug.out("Unknown parameter '" + name + "' - must be defined in DownloadManagerState");

                    return (0);

                } else {

                    // default overrides

                    // **** note - if you add to these make sure you extend the parameter listeners
                    // registered as well (see static initialiser at top)

                    if (name == PARAM_MAX_UPLOADS_WHEN_SEEDING_ENABLED) {

                        if (COConfigurationManager.getBooleanParameter("enable.seedingonly.maxuploads")) {

                            value = new Boolean(true);
                        }

                    } else if (name == PARAM_MAX_UPLOADS_WHEN_SEEDING) {

                        int def = COConfigurationManager.getIntParameter("Max Uploads Seeding");

                        value = new Integer(def);

                    } else if (name == PARAM_MAX_UPLOADS) {

                        int def = COConfigurationManager.getIntParameter("Max Uploads");

                        value = new Integer(def);

                    } else if (name == PARAM_MAX_PEERS) {

                        int def = COConfigurationManager.getIntParameter("Max.Peer.Connections.Per.Torrent");

                        value = new Integer(def);

                    } else if (name == PARAM_MAX_PEERS_WHEN_SEEDING_ENABLED) {

                        if (COConfigurationManager.getBooleanParameter("Max.Peer.Connections.Per.Torrent.When.Seeding.Enable")) {

                            value = new Boolean(true);
                        }

                    } else if (name == PARAM_MAX_PEERS_WHEN_SEEDING) {

                        int def = COConfigurationManager.getIntParameter("Max.Peer.Connections.Per.Torrent.When.Seeding");

                        value = new Integer(def);

                    } else if (name == PARAM_MAX_SEEDS) {

                        value = new Integer(COConfigurationManager.getIntParameter("Max Seeds Per Torrent"));

                    } else if (name == PARAM_RANDOM_SEED) {

                        long rand = random.nextLong();

                        setLongParameter(name, rand);

                        value = new Long(rand);
                    }
                }
            }

            if (value instanceof Boolean) {

                return (((Boolean) value).booleanValue() ? 1 : 0);

            } else if (value instanceof Integer) {

                return (((Integer) value).longValue());

            } else if (value instanceof Long) {

                return (((Long) value).longValue());
            }

            Debug.out("Invalid parameter value for '" + name + "' - " + value);

            return (0);

        } finally {

            this_mon.exit();
        }
    }

    public void setLongParameter(String name, long value) {
        Object default_value = default_parameters.get(name);

        if (default_value == null) {

            Debug.out("Unknown parameter '" + name + "' - must be defined in DownloadManagerState");
        }

        try {
            this_mon.enter();

            // gotta clone here otherwise we update the underlying map and the setMapAttribute code
            // doesn't think it has changed

            parameters = new LightHashMap(parameters);

            parameters.put(name, new Long(value));

            setMapAttribute(AT_PARAMETERS, parameters);

        } finally {

            this_mon.exit();
        }
    }

    public int getIntParameter(String name) {
        return ((int) getLongParameter(name));
    }

    public void setIntParameter(String name, int value) {
        setLongParameter(name, value);
    }

    public boolean getBooleanParameter(String name) {
        return (getLongParameter(name) != 0);
    }

    public void setBooleanParameter(String name, boolean value) {
        setLongParameter(name, value ? 1 : 0);
    }

    public void setAttribute(String name, String value) {

        if (name.equals(AT_CATEGORY)) {

            if (value == null) {

                setCategory(null);

            } else {
                Category cat = CategoryManager.getCategory(value);

                if (cat == null) {

                    cat = CategoryManager.createCategory(value);

                }

                setCategory(cat);
            }
            return;
        }

        if (name.equals(AT_RELATIVE_SAVE_PATH)) {
            if (value.length() > 0) {
                File relative_path_file = new File(value);
                relative_path_file = DownloadManagerDefaultPaths.normaliseRelativePath(relative_path_file);
                value = (relative_path_file == null) ? "" : relative_path_file.getPath();
            }
        }

        setStringAttribute(name, value);
    }

    public String getAttribute(String name) {
        if (name.equals(AT_CATEGORY)) {

            Category cat = getCategory();

            if (cat == null) {

                return (null);
            }

            if (cat == CategoryManager.getCategory(Category.TYPE_UNCATEGORIZED)) {

                return (null);
            }

            return (cat.getName());

        } else {

            return (getStringAttribute(name));
        }
    }

    public Category getCategory() {
        return category;
    }

    public void setCategory(Category cat) {
        if (cat == category) {
            return;
        }

        if (cat != null && cat.getType() != Category.TYPE_USER) {

            cat = null;
            if (cat == category) {
                return;
            }
        }

        Category oldCategory = (category == null) ? CategoryManager.getCategory(Category.TYPE_UNCATEGORIZED) : category;

        category = cat;

        if (oldCategory != null) {

            oldCategory.removeManager(this);
        }

        DownloadManager dm = getDownloadManager();

        if (dm != null && !dm.isDestroyed()) {

            if (category != null) {

                category.addManager(this);

            } else {

                CategoryManager.getCategory(Category.TYPE_UNCATEGORIZED).addManager(this);
            }
        }

        if (category != null) {

            setStringAttribute(AT_CATEGORY, category.getName());

        } else {

            setStringAttribute(AT_CATEGORY, null);
        }
    }

    public String getTrackerClientExtensions() {
        return (getStringAttribute(AT_TRACKER_CLIENT_EXTENSIONS));
    }

    public void setTrackerClientExtensions(String value) {
        setStringAttribute(AT_TRACKER_CLIENT_EXTENSIONS, value);
    }

    public String getDisplayName() {
        return this.getStringAttribute(AT_DISPLAY_NAME);
    }

    public void setDisplayName(String value) {
        this.setStringAttribute(AT_DISPLAY_NAME, value);
    }

    public String getUserComment() {
        return this.getStringAttribute(AT_USER_COMMENT);
    }

    public void setUserComment(String value) {
        this.setStringAttribute(AT_USER_COMMENT, value);
    }

    public String getRelativeSavePath() {
        return this.getStringAttribute(AT_RELATIVE_SAVE_PATH);
    }

    public DiskManagerFileInfo getPrimaryFile() {
        int primaryIndex = -1;
        DiskManagerFileInfo[] fileInfo = download_manager.getDiskManagerFileInfoSet().getFiles();
        if (hasAttribute(AT_PRIMARY_FILE_IDX)) {
            primaryIndex = getIntAttribute(AT_PRIMARY_FILE_IDX);
        }

        if (primaryIndex < 0 || primaryIndex >= fileInfo.length) {
            primaryIndex = -1;
            if (fileInfo.length > 0) {
                int idxBiggest = -1;
                long lBiggest = -1;
                int numChecked = 0;
                for (int i = 0; i < fileInfo.length && numChecked < 10; i++) {
                    if (!fileInfo[i].isSkipped()) {
                        numChecked++;
                        if (fileInfo[i].getLength() > lBiggest) {
                            lBiggest = fileInfo[i].getLength();
                            idxBiggest = i;
                        }
                    }
                }
                if (idxBiggest >= 0) {
                    primaryIndex = idxBiggest;
                }
            }
            if (primaryIndex >= 0) {
                setPrimaryFile(fileInfo[primaryIndex]);
            }
        }

        if (primaryIndex >= 0) {
            return fileInfo[primaryIndex];
        }
        return null;
    }

    /**
     * @param primary
     */
    public void setPrimaryFile(DiskManagerFileInfo dmfi) {
        setIntAttribute(AT_PRIMARY_FILE_IDX, dmfi.getIndex());
    }

    public String[] getNetworks() {
        List values = getListAttributeSupport(AT_NETWORKS);

        List res = new ArrayList();

        // map back to the constants to allow == comparisons

        for (int i = 0; i < values.size(); i++) {

            String nw = (String) values.get(i);

            for (int j = 0; j < AENetworkClassifier.AT_NETWORKS.length; j++) {

                String nn = AENetworkClassifier.AT_NETWORKS[j];

                if (nn.equals(nw)) {

                    res.add(nn);
                }
            }
        }

        String[] x = new String[res.size()];

        res.toArray(x);

        return (x);
    }

    public boolean isNetworkEnabled(String network) {
        List values = getListAttributeSupport(AT_NETWORKS);
        return values.contains(network);
    }

    public void setNetworks(String[] networks) {
        if (networks == null) {

            networks = new String[0];
        }

        List l = new ArrayList();

        for (int i = 0; i < networks.length; i++) {

            l.add(networks[i]);
        }

        setListAttribute(AT_NETWORKS, l);
    }

    public void setNetworkEnabled(String network, boolean enabled) {
        List values = getListAttributeSupport(AT_NETWORKS);
        boolean alreadyEnabled = values.contains(network);
        List l = new ArrayList();

        if (enabled && !alreadyEnabled) {
            for (int i = 0; i < values.size(); i++) {
                l.add(values.get(i));
            }
            l.add(network);
            setListAttribute(AT_NETWORKS, l);
        }
        if (!enabled && alreadyEnabled) {
            for (int i = 0; i < values.size(); i++) {
                l.add(values.get(i));
            }
            l.remove(network);
            setListAttribute(AT_NETWORKS, l);
        }
    }

    // peer sources

    public String[] getPeerSources() {
        List values = getListAttributeSupport(AT_PEER_SOURCES);

        List res = new ArrayList();

        // map back to the constants to allow == comparisons

        for (int i = 0; i < values.size(); i++) {

            String ps = (String) values.get(i);

            for (int j = 0; j < PEPeerSource.PS_SOURCES.length; j++) {

                String x = PEPeerSource.PS_SOURCES[j];

                if (x.equals(ps)) {

                    res.add(x);
                }
            }
        }

        String[] x = new String[res.size()];

        res.toArray(x);

        return (x);
    }

    public boolean isPeerSourceEnabled(String peerSource) {
        List values = getListAttributeSupport(AT_PEER_SOURCES);

        return values.contains(peerSource);
    }

    public boolean isPeerSourcePermitted(String peerSource) {
        // no DHT for private torrents or explicitly prevented

        if (peerSource.equals(PEPeerSource.PS_DHT)) {

            if (TorrentUtils.getPrivate(torrent) || !TorrentUtils.getDHTBackupEnabled(torrent)) {

                return (false);
            }
        }

        // no PEX for private torrents

        if (peerSource.equals(PEPeerSource.PS_OTHER_PEER)) {

            if (TorrentUtils.getPrivate(torrent)) {

                return (false);
            }
        }

        List values = getListAttributeSupport(AT_PEER_SOURCES_DENIED);

        if (values != null) {

            if (values.contains(peerSource)) {

                return (false);
            }
        }

        return (true);
    }

    public void setPeerSourcePermitted(String peerSource, boolean enabled) {
        if (!getFlag(FLAG_ALLOW_PERMITTED_PEER_SOURCE_CHANGES)) {

            Logger.log(new LogEvent(torrent, LOGID, "Attempt to modify permitted peer sources denied as disabled '"
                    + TorrentUtils.getLocalisedName(torrent) + "'"));

            return;
        }

        if (!enabled) {

            setPeerSourceEnabled(peerSource, false);
        }

        List values = getListAttributeSupport(AT_PEER_SOURCES_DENIED);

        if (values == null) {

            if (!enabled) {

                values = new ArrayList();

                values.add(peerSource);

                setListAttribute(AT_PEER_SOURCES_DENIED, values);
            }
        } else {

            if (enabled) {

                values.remove(peerSource);

            } else {

                if (!values.contains(peerSource)) {

                    values.add(peerSource);
                }
            }

            setListAttribute(AT_PEER_SOURCES_DENIED, values);
        }
    }

    public void setPeerSources(String[] ps) {
        if (ps == null) {

            ps = new String[0];
        }

        List l = new ArrayList();

        for (int i = 0; i < ps.length; i++) {

            String p = ps[i];

            if (isPeerSourcePermitted(p)) {

                l.add(ps[i]);
            }
        }

        setListAttribute(AT_PEER_SOURCES, l);
    }

    public void setPeerSourceEnabled(String source, boolean enabled) {
        if (enabled && !isPeerSourcePermitted(source)) {

            return;
        }

        List values = getListAttributeSupport(AT_PEER_SOURCES);

        boolean alreadyEnabled = values.contains(source);

        List l = new ArrayList();

        if (enabled && !alreadyEnabled) {
            for (int i = 0; i < values.size(); i++) {
                l.add(values.get(i));
            }
            l.add(source);
            setListAttribute(AT_PEER_SOURCES, l);
        }
        if (!enabled && alreadyEnabled) {
            for (int i = 0; i < values.size(); i++) {
                l.add(values.get(i));
            }
            l.remove(source);
            setListAttribute(AT_PEER_SOURCES, l);
        }
    }

    // links stuff

    private volatile WeakReference<LinkFileMap> file_link_cache = null;

    public void setFileLink(int source_index, File link_source, File link_destination) {
        LinkFileMap links = getFileLinks();

        File existing = (File) links.get(source_index, link_source);

        if (link_destination == null) {

            if (existing == null) {

                return;
            }
        } else if (existing != null && existing.equals(link_destination)) {

            return;
        }

        links.put(source_index, link_source, link_destination);

        List list = new ArrayList();

        Iterator<LinkFileMap.Entry> it = links.entryIterator();

        while (it.hasNext()) {

            LinkFileMap.Entry entry = it.next();

            int index = entry.getIndex();
            File source = entry.getFromFile();
            File target = entry.getToFile();

            String str = index + "\n" + source + "\n" + (target == null ? "" : target.toString());

            list.add(str);
        }

        // System.out.println( "setFileLink: " + link_source + " -> " + link_destination );

        synchronized (this) {

            file_link_cache = new WeakReference<LinkFileMap>(links);
        }

        setListAttribute(AT_FILE_LINKS2, list);
    }

    public void setFileLinks(List<Integer> source_indexes, List<File> link_sources, List<File> link_destinations) {
        LinkFileMap links = getFileLinks();

        boolean changed = false;

        for (int i = 0; i < link_sources.size(); i++) {

            int source_index = source_indexes.get(i);
            File link_source = link_sources.get(i);
            File link_destination = link_destinations.get(i);

            File existing = links.get(source_index, link_source);

            if (link_destination == null) {

                if (existing == null) {

                    continue;
                }
            } else if (existing != null && existing.equals(link_destination)) {

                continue;
            }

            links.put(source_index, link_source, link_destination);

            changed = true;
        }

        if (!changed) {

            return;
        }

        // System.out.println( "setFileLinks: " + links.getString());

        List list = new ArrayList();

        Iterator<LinkFileMap.Entry> it = links.entryIterator();

        while (it.hasNext()) {

            LinkFileMap.Entry entry = it.next();

            int index = entry.getIndex();
            File source = entry.getFromFile();
            File target = entry.getToFile();

            String str = index + "\n" + source + "\n" + (target == null ? "" : target.toString());

            list.add(str);
        }

        synchronized (this) {

            file_link_cache = new WeakReference<LinkFileMap>(links);
        }

        setListAttribute(AT_FILE_LINKS2, list);
    }

    public void clearFileLinks() {
        LinkFileMap links = getFileLinks();

        List list = new ArrayList();

        Iterator<LinkFileMap.Entry> it = links.entryIterator();

        boolean changed = false;

        while (it.hasNext()) {

            LinkFileMap.Entry entry = it.next();

            int index = entry.getIndex();
            File source = entry.getFromFile();
            File target = entry.getToFile();

            if (target != null) {

                changed = true;
            }

            String str = index + "\n" + source + "\n";

            list.add(str);
        }

        if (changed) {

            synchronized (this) {

                file_link_cache = null;
            }

            setListAttribute(AT_FILE_LINKS2, list);
        }

        // System.out.println( "clearFileLinks" );
    }

    public File getFileLink(int source_index, File link_source) {
        LinkFileMap map = null;

        WeakReference<LinkFileMap> ref = file_link_cache;

        if (ref != null) {

            map = ref.get();
        }

        if (map == null) {

            map = getFileLinks();

            synchronized (this) {

                file_link_cache = new WeakReference<LinkFileMap>(map);
            }
        }

        File res = map.get(source_index, link_source);

        // System.out.println( "getFileLink: " + link_source + " -> " + res );

        return (res);
    }

    public LinkFileMap getFileLinks() {
        LinkFileMap map = null;

        WeakReference<LinkFileMap> ref = file_link_cache;

        if (ref != null) {

            map = ref.get();
        }

        if (map == null) {

            map = getFileLinksSupport();

            synchronized (this) {

                file_link_cache = new WeakReference<LinkFileMap>(map);
            }
        }

        return (map);
    }

    private LinkFileMap getFileLinksSupport() {
        LinkFileMap res = new LinkFileMap();

        List new_values = getListAttributeSupport(AT_FILE_LINKS2);

        if (new_values.size() > 0) {

            for (int i = 0; i < new_values.size(); i++) {

                String entry = (String) new_values.get(i);

                String[] bits = entry.split("\n");

                if (bits.length >= 2) {

                    try {
                        int index = Integer.parseInt(bits[0].trim());
                        File source = new File(bits[1].trim());
                        File target = bits.length < 3 ? null : new File(bits[2].trim());

                        if (index >= 0) {

                            res.put(index, source, target);

                        } else {

                            // can get here when partially resolved link state is saved and then re-read

                            res.putMigration(source, target);
                        }
                    } catch (Throwable e) {

                        Debug.out(e);
                    }
                }
            }
        } else {

            List old_values = getListAttributeSupport(AT_FILE_LINKS_DEPRECATED);

            for (int i = 0; i < old_values.size(); i++) {

                String entry = (String) old_values.get(i);

                int sep = entry.indexOf("\n");

                if (sep != -1) {

                    File target = (sep == entry.length() - 1) ? null : new File(entry.substring(sep + 1));

                    res.putMigration(new File(entry.substring(0, sep)), target);
                }
            }
        }

        // System.out.println( "getFileLinks: " + res.getString());

        return (res);
    }

    public boolean isOurContent() {
        // HACK!

        Map mapAttr = getMapAttribute("Plugin.azdirector.ContentMap");

        return mapAttr != null && mapAttr.containsKey("DIRECTOR PUBLISH");
    }

    // general stuff

    protected String getStringAttribute(String attribute_name) {
        informWillRead(attribute_name);

        try {
            this_mon.enter();

            if (!(attributes.get(attribute_name) instanceof byte[])) {

                return (null);
            }

            byte[] bytes = (byte[]) attributes.get(attribute_name);

            if (bytes == null) {

                return (null);
            }

            try {
                return (new String(bytes, Constants.DEFAULT_ENCODING));

            } catch (UnsupportedEncodingException e) {

                Debug.printStackTrace(e);

                return (null);
            }
        } finally {

            this_mon.exit();
        }
    }

    protected void setStringAttribute(final String attribute_name, final String attribute_value) {
        boolean changed = false;

        try {
            this_mon.enter();

            if (attribute_value == null) {

                if (attributes.containsKey(attribute_name)) {

                    attributes.remove(attribute_name);

                    write_required = changed = true;
                }
            } else {

                try {
                    byte[] existing_bytes = (byte[]) attributes.get(attribute_name);

                    byte[] new_bytes = attribute_value.getBytes(Constants.DEFAULT_ENCODING);

                    if (existing_bytes == null || !Arrays.equals(existing_bytes, new_bytes)) {

                        attributes.put(attribute_name, new_bytes);

                        write_required = changed = true;
                    }

                } catch (UnsupportedEncodingException e) {

                    Debug.printStackTrace(e);
                }
            }
        } finally {

            this_mon.exit();
        }

        if (changed) {

            informWritten(attribute_name);
        }
    }

    public long getLongAttribute(String attribute_name) {
        informWillRead(attribute_name);

        try {
            this_mon.enter();

            Long l = (Long) attributes.get(attribute_name);

            if (l == null) {

                Object def = default_attributes.get(attribute_name);

                if (def != null) {

                    if (def instanceof Long) {

                        return (((Long) def).longValue());

                    } else if (def instanceof Integer) {

                        return (((Integer) def).longValue());

                    } else {

                        Debug.out("unknown default type " + def);
                    }
                } else if (attribute_name == AT_FILES_EXPANDED) {

                    boolean featured = TorrentUtils.isFeaturedContent(torrent);

                    long res = featured ? 1 : 0;

                    attributes.put(attribute_name, new Long(res));

                    write_required = true;

                    return (res);
                }

                return (0);
            }

            return (l.longValue());

        } finally {

            this_mon.exit();
        }
    }

    public void setLongAttribute(final String attribute_name, final long attribute_value) {
        boolean changed = false;

        try {
            this_mon.enter();

            Long existing_value = (Long) attributes.get(attribute_name);

            if (existing_value == null || existing_value.longValue() != attribute_value) {

                attributes.put(attribute_name, new Long(attribute_value));

                write_required = changed = true;
            }
        } finally {

            this_mon.exit();
        }

        if (changed) {

            informWritten(attribute_name);
        }
    }

    public void setListAttribute(String name, String[] values) {
        List list = values == null ? null : Arrays.asList((Object[]) values.clone());
        /*
         * if ( list != null ){ for (int i=0;i<values.length;i++){ list.add( values[i]); } }
         */

        setListAttribute(name, list);
    }

    public String getListAttribute(String name, int idx) {
        if (name.equals(AT_NETWORKS) || name.equals(AT_PEER_SOURCES))
            throw new UnsupportedOperationException("not supported right now, implement it yourself :P");

        informWillRead(name);

        try {
            this_mon.enter();
            List values = (List) attributes.get(name);
            if (values == null || idx >= values.size() || idx < 0)
                return null;
            Object o = values.get(idx);
            if (o instanceof byte[]) {
                byte[] bytes = (byte[]) o;
                String s = null;
                try {
                    s = StringInterner.intern(new String(bytes, Constants.DEFAULT_ENCODING));
                } catch (UnsupportedEncodingException e) {
                    Debug.printStackTrace(e);
                }
                if (s != null)
                    values.set(idx, s);
                return s;
            } else if (o instanceof String) {
                return (String) o;
            }
        } finally {
            this_mon.exit();
        }

        return null;
    }

    public String[] getListAttribute(String attribute_name) {
        if (attribute_name == AT_NETWORKS) {

            return (getNetworks());

        } else if (attribute_name == AT_PEER_SOURCES) {

            return (getPeerSources());

        } else {

            List l = getListAttributeSupport(attribute_name);

            if (l == null) {

                return (null);
            }

            String[] res = new String[l.size()];

            try {
                res = (String[]) l.toArray(res);
            } catch (ArrayStoreException e) {
                Debug.out("getListAttribute( " + attribute_name + ") - object isnt String - " + e);

                return (null);
            }

            return (res);
        }
    }

    protected List getListAttributeSupport(String attribute_name) {
        informWillRead(attribute_name);

        try {
            this_mon.enter();

            List values = (List) attributes.get(attribute_name);

            List res = new ArrayList(values != null ? values.size() : 0);

            if (values != null) {

                for (int i = 0; i < values.size(); i++) {

                    Object o = values.get(i);

                    if (o instanceof byte[]) {

                        byte[] bytes = (byte[]) o;

                        String s = null;

                        try {

                            s = StringInterner.intern(new String(bytes, Constants.DEFAULT_ENCODING));

                        } catch (UnsupportedEncodingException e) {

                            Debug.printStackTrace(e);
                        }

                        if (s != null) {
                            res.add(s);
                            values.set(i, s);
                        }
                    } else if (o instanceof String) {

                        res.add(o);
                    }
                }
            }

            return (res);

        } finally {

            this_mon.exit();
        }
    }

    protected void setListAttribute(final String attribute_name, final List attribute_value) {
        boolean changed = false;

        try {
            this_mon.enter();

            if (attribute_value == null) {

                if (attributes.containsKey(attribute_name)) {

                    attributes.remove(attribute_name);

                    write_required = changed = true;
                }
            } else {

                List old_value = getListAttributeSupport(attribute_name);

                if (old_value == null || old_value.size() != attribute_value.size()) {

                    attributes.put(attribute_name, attribute_value);

                    write_required = changed = true;

                } else {

                    if (old_value == attribute_value) {

                        Debug.out("setListAttribute: should clone?");
                    }

                    changed = !BEncoder.listsAreIdentical(old_value, attribute_value);

                    if (changed) {

                        write_required = true;

                        attributes.put(attribute_name, attribute_value);
                    }
                }
            }
        } finally {

            this_mon.exit();
        }

        if (changed) {

            informWritten(attribute_name);
        }
    }

    public Map getMapAttribute(String attribute_name) {
        informWillRead(attribute_name);

        try {
            this_mon.enter();

            Map value = (Map) attributes.get(attribute_name);

            return (value);

        } finally {

            this_mon.exit();
        }
    }

    public void setMapAttribute(final String attribute_name, final Map attribute_value) {
        setMapAttribute(attribute_name, attribute_value, false);
    }

    protected void setMapAttribute(final String attribute_name, final Map attribute_value, boolean disable_change_notification) {
        boolean changed = false;

        try {
            this_mon.enter();

            if (attribute_value == null) {

                if (attributes.containsKey(attribute_name)) {

                    attributes.remove(attribute_name);

                    write_required = changed = true;
                }
            } else {

                Map old_value = getMapAttribute(attribute_name);

                if (old_value == null || old_value.size() != attribute_value.size()) {

                    attributes.put(attribute_name, attribute_value);

                    write_required = changed = true;

                } else {

                    if (old_value == attribute_value) {

                        Debug.out("setMapAttribute: should clone?");
                    }

                    changed = !BEncoder.mapsAreIdentical(old_value, attribute_value);

                    if (changed) {

                        write_required = true;

                        attributes.put(attribute_name, attribute_value);
                    }
                }
            }
        } finally {

            this_mon.exit();
        }

        if (changed && !disable_change_notification) {

            informWritten(attribute_name);
        }
    }

    public boolean hasAttribute(String name) {
        try {

            this_mon.enter();

            if (attributes == null) {
                return false;
            }

            return attributes.containsKey(name);

        } finally {

            this_mon.exit();
        }
    }

    // These methods just use long attributes to store data into.

    public void setIntAttribute(String name, int value) {
        setLongAttribute(name, value);
    }

    public int getIntAttribute(String name) {
        return (int) getLongAttribute(name);
    }

    public void setBooleanAttribute(String name, boolean value) {
        setLongAttribute(name, (value ? 1 : 0));
    }

    public boolean getBooleanAttribute(String name) {
        return getLongAttribute(name) != 0;
    }

    public static DownloadManagerState getDownloadState(DownloadManager dm) {
        return (new nullState(dm));
    }

    protected void informWritten(final String attribute_name) {
        // don't make any of this async as the link management code for cache files etc
        // relies on callbacks here being synchronous...

        List listeners_ref = listeners_cow.getList();

        for (int i = 0; i < listeners_ref.size(); i++) {

            try {
                ((DownloadManagerStateListener) listeners_ref.get(i)).stateChanged(this, new DownloadManagerStateEvent() {
                    public int getType() {
                        return (DownloadManagerStateEvent.ET_ATTRIBUTE_WRITTEN);
                    }

                    public Object getData() {
                        return (attribute_name);
                    }
                });
            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }

        listeners_ref = null;
        CopyOnWriteList write_listeners = (CopyOnWriteList) listeners_write_map_cow.get(attribute_name);
        if (write_listeners != null) {
            listeners_ref = write_listeners.getList();
        }

        if (listeners_ref != null) {
            for (int i = 0; i < listeners_ref.size(); i++) {
                try {
                    ((DownloadManagerStateAttributeListener) listeners_ref.get(i)).attributeEventOccurred(download_manager, attribute_name,
                            DownloadManagerStateAttributeListener.WRITTEN);
                } catch (Throwable t) {
                    Debug.printStackTrace(t);
                }
            }
        }

    }

    protected void informWillRead(final String attribute_name) {
        // avoid potential recursion will a will-be-read causing a write that then
        // causes a further will-be-read...

        List will_be_read_list = (List) tls_wbr.get();

        if (!will_be_read_list.contains(attribute_name)) {

            will_be_read_list.add(attribute_name);

            try {

                List listeners_ref = listeners_cow.getList();

                for (int i = 0; i < listeners_ref.size(); i++) {

                    try {
                        ((DownloadManagerStateListener) listeners_ref.get(i)).stateChanged(this, new DownloadManagerStateEvent() {
                            public int getType() {
                                return (DownloadManagerStateEvent.ET_ATTRIBUTE_WILL_BE_READ);
                            }

                            public Object getData() {
                                return (attribute_name);
                            }
                        });
                    } catch (Throwable e) {

                        Debug.printStackTrace(e);
                    }
                }

                listeners_ref = null;
                CopyOnWriteList read_listeners = null;

                read_listeners = (CopyOnWriteList) listeners_read_map_cow.get(attribute_name);
                if (read_listeners != null) {
                    listeners_ref = read_listeners.getList();
                }

                if (listeners_ref != null) {
                    for (int i = 0; i < listeners_ref.size(); i++) {
                        try {
                            ((DownloadManagerStateAttributeListener) listeners_ref.get(i)).attributeEventOccurred(download_manager, attribute_name,
                                    DownloadManagerStateAttributeListener.WILL_BE_READ);
                        } catch (Throwable t) {
                            Debug.printStackTrace(t);
                        }
                    }
                }
            } finally {

                will_be_read_list.remove(attribute_name);
            }
        }
    }

    public void addListener(DownloadManagerStateListener l) {
        listeners_cow.add(l);
    }

    public void removeListener(DownloadManagerStateListener l) {
        listeners_cow.remove(l);
    }

    public void addListener(DownloadManagerStateAttributeListener l, String attribute, int event_type) {
        CopyOnWriteMap map_to_use =
                (event_type == DownloadManagerStateAttributeListener.WILL_BE_READ) ? this.listeners_read_map_cow : this.listeners_write_map_cow;
        CopyOnWriteList lst = (CopyOnWriteList) map_to_use.get(attribute);
        if (lst == null) {
            lst = new CopyOnWriteList();
            map_to_use.put(attribute, lst);
        }
        lst.add(l);
    }

    public void removeListener(DownloadManagerStateAttributeListener l, String attribute, int event_type) {
        CopyOnWriteMap map_to_use =
                (event_type == DownloadManagerStateAttributeListener.WILL_BE_READ) ? this.listeners_read_map_cow : this.listeners_write_map_cow;
        CopyOnWriteList lst = (CopyOnWriteList) map_to_use.get(attribute);
        if (lst != null) {
            lst.remove(l);
        }
    }

    public void generateEvidence(IndentWriter writer) {
        writer.println("DownloadManagerState");

        try {
            writer.indent();

            writer.println("parameters=" + parameters);
            writer.println("flags=" + getFlags());
            DiskManagerFileInfo primaryFile = getPrimaryFile();
            if (primaryFile != null) {
                writer.println("primary file=" + Debug.secretFileName(primaryFile.getFile(true).getAbsolutePath()));
            }

        } finally {

            writer.exdent();
        }
    }

    public void dump(IndentWriter writer) {
        writer.println("attributes: " + parameters);
    }

    protected static class nullState implements DownloadManagerState {

        protected DownloadManager download_manager;

        protected nullState(DownloadManager _dm) {
            download_manager = _dm;
        }

        public TOTorrent getTorrent() {
            return (null);
        }

        public File getStateFile() {
            return (null);
        }

        public DownloadManager getDownloadManager() {
            return (download_manager);
        }

        public void clearResumeData() {
        }

        public Map getResumeData() {
            return (new HashMap());
        }

        public void setResumeData(Map data) {
        }

        public boolean isResumeDataComplete() {
            return (false);
        }

        public void clearTrackerResponseCache() {
        }

        public Map getTrackerResponseCache() {
            return (new HashMap());
        }

        public void setTrackerResponseCache(Map value) {
        }

        public void setFlag(long flag, boolean set) {
        }

        public boolean getFlag(long flag) {
            return (false);
        }

        public long getFlags() {
            return 0;
        }

        public void setParameterDefault(String name) {
        }

        public long getLongParameter(String name) {
            return (0);
        }

        public void setLongParameter(String name, long value) {
        }

        public int getIntParameter(String name) {
            return (0);
        }

        public void setIntParameter(String name, int value) {
        }

        public boolean getBooleanParameter(String name) {
            return (false);
        }

        public void setBooleanParameter(String name, boolean value) {
        }

        public void setAttribute(String name, String value) {
        }

        public String getAttribute(String name) {
            return (null);
        }

        public String getTrackerClientExtensions() {
            return (null);
        }

        public void setTrackerClientExtensions(String value) {
        }

        public void setListAttribute(String name, String[] values) {
        }

        public String getListAttribute(String name, int idx) {
            return null;
        }

        public String[] getListAttribute(String name) {
            return (null);
        }

        public void setMapAttribute(String name, Map value) {
        }

        public Map getMapAttribute(String name) {
            return (null);
        }

        public boolean hasAttribute(String name) {
            return false;
        }

        public int getIntAttribute(String name) {
            return 0;
        }

        public long getLongAttribute(String name) {
            return 0L;
        }

        public boolean getBooleanAttribute(String name) {
            return false;
        }

        public void setIntAttribute(String name, int value) {
        }

        public void setLongAttribute(String name, long value) {
        }

        public void setBooleanAttribute(String name, boolean value) {
        }

        public Category getCategory() {
            return (null);
        }

        public void setCategory(Category cat) {
        }

        public String[] getNetworks() {
            return (new String[0]);
        }

        public boolean isNetworkEnabled(String network) {
            return false;
        }

        public void setNetworks(String[] networks) {
        }

        public void setNetworkEnabled(String network, boolean enabled) {
        }

        public String[] getPeerSources() {
            return (new String[0]);
        }

        public boolean isPeerSourcePermitted(String peerSource) {
            return (false);
        }

        public void setPeerSourcePermitted(String peerSource, boolean permitted) {
        }

        public boolean isPeerSourceEnabled(String peerSource) {
            return false;
        }

        public void suppressStateSave(boolean suppress) {
        }

        public void setPeerSources(String[] networks) {
        }

        public void setPeerSourceEnabled(String source, boolean enabled) {
        }

        public void setFileLink(int source_index, File link_source, File link_destination) {
        }

        public void setFileLinks(List<Integer> source_indexes, List<File> link_sources, List<File> link_destinations) {
        }

        public void clearFileLinks() {
        }

        public File getFileLink(int source_index, File link_source) {
            return (null);
        }

        public LinkFileMap getFileLinks() {
            return (new LinkFileMap());
        }

        public void setActive(boolean active) {
        }

        public void discardFluff() {
        }

        public boolean exportState(File target_dir) {
            return (false);
        }

        public void save() {
        }

        public void delete() {
        }

        public void addListener(DownloadManagerStateListener l) {
        }

        public void removeListener(DownloadManagerStateListener l) {
        }

        public void addListener(DownloadManagerStateAttributeListener l, String attribute, int event_type) {
        }

        public void removeListener(DownloadManagerStateAttributeListener l, String attribute, int event_type) {
        }

        public void setDisplayName(String name) {
        }

        public String getDisplayName() {
            return null;
        }

        public void setUserComment(String name) {
        }

        public String getUserComment() {
            return null;
        }

        public void setRelativeSavePath(String name) {
        }

        public String getRelativeSavePath() {
            return null;
        }

        public boolean parameterExists(String name) {
            // TODO Auto-generated method stub
            return false;
        }

        public void generateEvidence(IndentWriter writer) {
            writer.println("DownloadManagerState: broken torrent");
        }

        public void dump(IndentWriter writer) {
        }

        public boolean isOurContent() {
            // TODO Auto-generated method stub
            return false;
        }

        // @see org.gudy.azureus2.core3.download.DownloadManagerState#getPrimaryFile()
        public DiskManagerFileInfo getPrimaryFile() {
            // TODO Auto-generated method stub
            return null;
        }

        // @see org.gudy.azureus2.core3.download.DownloadManagerState#setPrimaryFile(org.gudy.azureus2.core3.disk.DiskManagerFileInfo)
        public void setPrimaryFile(DiskManagerFileInfo dmfi) {
            // TODO Auto-generated method stub

        }
    }

    protected static class CachedStateWrapper extends LogRelation implements TorrentUtils.ExtendedTorrent {
        private DownloadManagerImpl download_manager;

        private String torrent_file;
        private HashWrapper torrent_hash_wrapper;
        private Map cache;
        private Map cache_attributes;
        private Map cache_azp;

        private volatile TorrentUtils.ExtendedTorrent delegate;
        private TOTorrentException fixup_failure;

        private boolean discard_pieces;
        private boolean logged_failure;

        private Boolean simple_torrent;
        private long size;
        private int file_count;

        private URL announce_url;
        private cacheGroup announce_group;

        private volatile boolean discard_fluff;

        protected CachedStateWrapper(DownloadManagerImpl _download_manager, String _torrent_file, byte[] _torrent_hash, Map _cache,
                boolean _force_piece_discard) {
            download_manager = _download_manager;
            torrent_file = _torrent_file;
            torrent_hash_wrapper = new HashWrapper(_torrent_hash);
            cache = _cache;

            cache_attributes = (Map) cache.get("attributes");
            cache_azp = (Map) cache.get("azp");

            if (_force_piece_discard) {

                discard_pieces = true;

            } else {

                Long l_fp = (Long) cache.get("dp");

                if (l_fp != null) {

                    discard_pieces = l_fp.longValue() == 1;
                }
            }

            Long st = (Long) cache.get("simple");

            if (st != null) {

                simple_torrent = new Boolean(st.longValue() == 1);
            }

            Long fc = (Long) cache.get("fc");

            if (fc != null) {

                file_count = fc.intValue();
            }

            Long l_size = (Long) cache.get("size");

            if (l_size != null) {

                size = l_size.longValue();
            }

            byte[] au = (byte[]) cache.get("au");

            if (au != null) {

                try {
                    announce_url = StringInterner.internURL(new URL((new String(au, "UTF-8"))));

                } catch (Throwable e) {

                }
            }

            List ag = (List) cache.get("ag");

            if (ag != null) {

                try {
                    announce_group = importGroup(ag);

                } catch (Throwable e) {

                }
            }
        }

        protected static Map export(DownloadManagerState dms)

        throws TOTorrentException {
            Map cache = new HashMap();

            TOTorrent state = dms.getTorrent();

            cache.put("hash", state.getHash());
            cache.put("name", state.getName());
            cache.put("utf8name", state.getUTF8Name() == null ? "" : state.getUTF8Name());
            cache.put("comment", state.getComment());
            cache.put("createdby", state.getCreatedBy());
            cache.put("size", new Long(state.getSize()));

            cache.put("encoding", state.getAdditionalStringProperty("encoding"));
            cache.put("torrent filename", state.getAdditionalStringProperty("torrent filename"));

            cache.put("attributes", state.getAdditionalMapProperty(ATTRIBUTE_KEY));
            cache.put("azp", state.getAdditionalMapProperty(AZUREUS_PROPERTIES_KEY));

            try {
                cache.put("au", state.getAnnounceURL().toExternalForm());
                cache.put("ag", exportGroup(state.getAnnounceURLGroup()));

            } catch (Throwable e) {
            }

            boolean discard_pieces = dms.isResumeDataComplete();

            TOTorrent t = dms.getTorrent();

            if (t instanceof CachedStateWrapper) {

                // We don't want to force a fixup here when saving!

                CachedStateWrapper csw = (CachedStateWrapper) t;

                if (!discard_pieces) {

                    // discard pieces if they are currently discarded

                    discard_pieces = csw.peekPieces() == null;
                }

                Boolean simple_torrent = csw.simple_torrent;

                if (simple_torrent != null) {

                    cache.put("simple", new Long(simple_torrent.booleanValue() ? 1 : 0));

                } else {

                    Debug.out("Failed to cache simple state");
                }

                int fc = csw.file_count;

                if (fc > 0) {

                    cache.put("fc", new Long(fc));
                }
            } else {
                if (t instanceof TorrentUtils.torrentDelegate) {

                    // torrent is already 'fixed up' so no harm in directly grabbing stuff for export as already loaded

                    cache.put("simple", new Long(t.isSimpleTorrent() ? 1 : 0));

                    cache.put("fc", t.getFileCount());

                } else {

                    Debug.out("Hmm, torrent isn't cache-state-wrapper, it is " + t);
                }
            }

            cache.put("dp", new Long(discard_pieces ? 1 : 0));

            return (cache);
        }

        protected static List exportGroup(TOTorrentAnnounceURLGroup group) {
            TOTorrentAnnounceURLSet[] sets = group.getAnnounceURLSets();

            List result = new ArrayList();

            for (int i = 0; i < sets.length; i++) {

                TOTorrentAnnounceURLSet set = sets[i];

                URL[] urls = set.getAnnounceURLs();

                if (urls.length > 0) {

                    List s = new ArrayList(urls.length);

                    for (int j = 0; j < urls.length; j++) {

                        s.add(urls[j].toExternalForm());
                    }

                    result.add(s);
                }
            }

            return (result);
        }

        protected cacheGroup importGroup(List l)

        throws Exception {
            return (new cacheGroup(l));
        }

        protected class cacheGroup implements TOTorrentAnnounceURLGroup {
            private TOTorrentAnnounceURLSet[] sets;

            protected cacheGroup(List group)

            throws Exception {
                sets = new TOTorrentAnnounceURLSet[group.size()];

                for (int i = 0; i < sets.length; i++) {

                    List set = (List) group.get(i);

                    URL[] urls = new URL[set.size()];

                    for (int j = 0; j < urls.length; j++) {

                        urls[j] = StringInterner.internURL(new URL(new String((byte[]) set.get(j), "UTF-8")));
                    }

                    sets[i] = new cacheSet(urls);
                }
            }

            public TOTorrentAnnounceURLSet[] getAnnounceURLSets() {
                if (announce_group == null && fixup()) {

                    return delegate.getAnnounceURLGroup().getAnnounceURLSets();
                }

                return (sets);
            }

            void fixGroup() {
                TOTorrentAnnounceURLSet[] realSets = delegate.getAnnounceURLGroup().getAnnounceURLSets();

                if (realSets.length != sets.length) {

                    // not a major issue - since we now allow the cached groups and persisted ones to
                    // drift (see DNS changes) the cached ones will be accurate and the persisted ones
                    // will be re-adjusted should they become visible
                    // Debug.out("Cached announce group state does not match real state");

                } else {

                    for (int i = 0; i < realSets.length; i++) {

                        if (sets[i] instanceof cacheSet) {

                            ((cacheSet) sets[i]).delegateSet = realSets[i];
                        }
                    }

                    sets = null;
                }
            }

            public void setAnnounceURLSets(TOTorrentAnnounceURLSet[] toSet) {
                if (fixup()) {

                    TOTorrentAnnounceURLSet[] modToSet = new TOTorrentAnnounceURLSet[toSet.length];

                    for (int i = 0; i < toSet.length; i++) {

                        TOTorrentAnnounceURLSet set = toSet[i];

                        if (set instanceof cacheSet) {

                            modToSet[i] = ((cacheSet) set).delegateSet;
                        }

                        if (modToSet[i] == null) {

                            modToSet[i] = set;
                        }
                    }

                    delegate.getAnnounceURLGroup().setAnnounceURLSets(modToSet);
                }
            }

            public TOTorrentAnnounceURLSet createAnnounceURLSet(URL[] urls) {
                if (fixup()) {

                    return (delegate.getAnnounceURLGroup().createAnnounceURLSet(urls));
                }

                return (null);
            }

            protected class cacheSet implements TOTorrentAnnounceURLSet {
                private URL[] urls;
                private TOTorrentAnnounceURLSet delegateSet;

                public cacheSet(URL[] urls) {
                    this.urls = urls;
                }

                public URL[] getAnnounceURLs() {
                    if (announce_group == null && fixup() && delegateSet != null) {

                        return delegateSet.getAnnounceURLs();
                    }

                    return (urls);
                }

                public void setAnnounceURLs(URL[] toSet) {
                    if (fixup() && delegateSet != null) {

                        delegateSet.setAnnounceURLs(toSet);

                    } else {

                        urls = toSet;
                    }
                }
            }
        }

        protected void clearCache() {
            cache = null;
        }

        protected boolean fixup() {
            try {
                if (delegate == null) {

                    synchronized (this) {

                        if (delegate == null) {

                            // System.out.println( "Fixing up " + this );

                            if (fixup_failure != null) {

                                throw (fixup_failure);
                            }

                            delegate = loadRealState();

                            if (discard_fluff) {

                                delegate.setDiscardFluff(discard_fluff);
                            }

                            if (cache != null) {

                                Debug.out("Cache miss forced fixup");
                            }

                            cache = null;

                            // join cache view back up with real state to save memory as the one
                            // we've just read is irrelevant due to the cache values being
                            // used

                            if (cache_attributes != null) {

                                delegate.setAdditionalMapProperty(ATTRIBUTE_KEY, cache_attributes);

                                cache_attributes = null;
                            }

                            if (cache_azp != null) {

                                delegate.setAdditionalMapProperty(AZUREUS_PROPERTIES_KEY, cache_azp);

                                cache_azp = null;
                            }

                            announce_url = null;

                            if (announce_group != null) {

                                announce_group.fixGroup();

                                announce_group = null;
                            }
                        }
                    }
                }

                return (true);

            } catch (TOTorrentException e) {

                fixup_failure = e;

                if (download_manager != null) {

                    download_manager.setTorrentInvalid(Debug.getNestedExceptionMessage(e));

                } else {

                    if (!logged_failure) {

                        logged_failure = true;

                        Debug.out("Torrent can't be loaded: " + Debug.getNestedExceptionMessage(e));
                    }
                }
            }

            return (false);
        }

        protected TorrentUtils.ExtendedTorrent loadRealState()

        throws TOTorrentException {
            // System.out.println("loadReal: " + torrent_file + " dp=" + discard_pieces + ": " + Debug.getCompressedStackTrace().substring(114));

            if (!SUPPRESS_FIXUP_ERRORS && Constants.isCVSVersion()) {

                if (Thread.currentThread().isDaemon()) {

                    // Debug.outNoStack( "Fixup on thread " + Thread.currentThread().getName() + ": " + Debug.getCompressedStackTrace());

                } else {

                    Debug.outNoStack(Debug.getCompressedStackTrace(new Exception() {
                        public String toString() {
                            return ("Premature fixup?");
                        }
                    }, 2, 10, true), true);
                }
            }

            File saved_file = getStateFile(torrent_hash_wrapper.getBytes());

            if (saved_file.exists()) {

                try {

                    return (TorrentUtils.readDelegateFromFile(saved_file, discard_pieces));

                } catch (Throwable e) {

                    Debug.out("Failed to load download state for " + saved_file);
                }
            }

            // try reading from original

            TOTorrent original_torrent = TorrentUtils.readFromFile(new File(torrent_file), true);

            torrent_hash_wrapper = original_torrent.getHashWrapper();

            saved_file = getStateFile(torrent_hash_wrapper.getBytes());

            if (saved_file.exists()) {

                try {
                    return (TorrentUtils.readDelegateFromFile(saved_file, discard_pieces));

                } catch (Throwable e) {

                    Debug.out("Failed to load download state for " + saved_file);
                }
            }

            // we must copy the torrent as we want one independent from the
            // original (someone might still have references to the original
            // and do stuff like write it somewhere else which would screw us
            // up)

            TorrentUtils.copyToFile(original_torrent, saved_file);

            return (TorrentUtils.readDelegateFromFile(saved_file, discard_pieces));
        }

        public byte[] getName() {
            Map c = cache;

            if (c != null) {

                byte[] name = (byte[]) c.get("name");
                if (name != null) {
                    return name;
                }
            }

            if (fixup()) {

                return (delegate.getName());
            }

            // Does grabbing the nested exception message always give us something useful?
            // My experience is that we just get an empty string here...
            return (("Error - " + Debug.getNestedExceptionMessage(fixup_failure)).getBytes());
        }

        public String getUTF8Name() {
            Map c = cache;

            if (c != null) {

                byte[] name = (byte[]) c.get("utf8name");
                if (name != null) {
                    String utf8name;
                    try {
                        utf8name = new String(name, "utf8");
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                    if (utf8name.length() == 0) {
                        return null;
                    }
                    return utf8name;
                }
            }

            if (fixup()) {
                return delegate.getUTF8Name();
            }
            return null;
        }

        public boolean isSimpleTorrent() {
            if (simple_torrent != null) {

                return (simple_torrent.booleanValue());
            }

            if (fixup()) {

                boolean st = delegate.isSimpleTorrent();

                simple_torrent = new Boolean(st);

                return (st);
            }

            return (false);
        }

        public byte[] getComment() {
            Map c = cache;

            if (c != null) {

                return ((byte[]) c.get("comment"));
            }

            if (fixup()) {

                return (delegate.getComment());
            }

            return (null);
        }

        public void setComment(String comment) {
            if (fixup()) {

                delegate.setComment(comment);
            }
        }

        public long getCreationDate() {
            if (fixup()) {

                return (delegate.getCreationDate());
            }

            return (0);
        }

        public void setCreationDate(long date) {
            if (fixup()) {

                delegate.setCreationDate(date);
            }
        }

        public byte[] getCreatedBy() {
            Map c = cache;

            if (c != null) {

                return ((byte[]) c.get("createdby"));
            }

            if (fixup()) {

                return (delegate.getCreatedBy());
            }

            return (null);
        }

        public void setCreatedBy(byte[] cb) {
            if (fixup()) {

                delegate.setCreatedBy(cb);
            }
        }

        public boolean isCreated() {
            if (fixup()) {

                return (delegate.isCreated());
            }

            return (false);
        }

        public boolean isDecentralised() {
            return (TorrentUtils.isDecentralised(getAnnounceURL()));
        }

        public URL getAnnounceURL() {
            if (announce_url != null) {

                return (announce_url);
            }

            if (fixup()) {

                return (delegate.getAnnounceURL());
            }

            return (null);
        }

        public boolean setAnnounceURL(URL url) {
            if (announce_url != null) {

                if (announce_url.toExternalForm().equals(url.toExternalForm())) {

                    return (false);
                }
            }

            if (fixup()) {

                return (delegate.setAnnounceURL(url));

            } else {

                announce_url = url;
            }

            return (false);
        }

        public TOTorrentAnnounceURLGroup getAnnounceURLGroup() {
            if (announce_group != null) {

                return (announce_group);
            }

            if (fixup()) {

                return (delegate.getAnnounceURLGroup());
            }

            return (null);
        }

        public byte[][] getPieces()

        throws TOTorrentException {
            if (fixup()) {

                return (delegate.getPieces());
            }

            throw (fixup_failure);
        }

        public void setPieces(byte[][] pieces)

        throws TOTorrentException {
            if (fixup()) {

                delegate.setPieces(pieces);

                return;
            }

            throw (fixup_failure);
        }

        public byte[][] peekPieces()

        throws TOTorrentException {
            if (fixup()) {

                return (delegate.peekPieces());
            }

            throw (fixup_failure);
        }

        public void setDiscardFluff(boolean discard) {
            discard_fluff = discard;

            if (delegate != null) {

                delegate.setDiscardFluff(discard_fluff);
            }
        }

        public long getPieceLength() {
            if (fixup()) {

                return (delegate.getPieceLength());
            }

            return (0);
        }

        public int getNumberOfPieces() {
            if (fixup()) {

                return (delegate.getNumberOfPieces());
            }

            return (0);
        }

        public long getSize() {
            if (size > 0) {

                return (size);
            }

            if (fixup()) {

                size = delegate.getSize();

                return (size);
            }

            return (0);
        }

        public int getFileCount() {
            if (file_count == 0) {

                if (fixup()) {

                    file_count = delegate.getFileCount();
                }
            }

            return (file_count);
        }

        public TOTorrentFile[] getFiles() {
            if (fixup()) {

                return (delegate.getFiles());
            }

            return (new TOTorrentFile[0]);
        }

        public byte[] getHash()

        throws TOTorrentException {
            // optimise this

            return (torrent_hash_wrapper.getBytes());
        }

        public HashWrapper getHashWrapper()

        throws TOTorrentException {
            return (torrent_hash_wrapper);
        }

        public void setHashOverride(byte[] hash)

        throws TOTorrentException {
            throw (new TOTorrentException("Not supported", TOTorrentException.RT_HASH_FAILS));
        }

        public boolean hasSameHashAs(TOTorrent other) {
            try {
                byte[] other_hash = other.getHash();

                return (Arrays.equals(getHash(), other_hash));

            } catch (TOTorrentException e) {

                Debug.printStackTrace(e);

                return (false);
            }
        }

        public boolean getPrivate() {
            if (fixup()) {

                return (delegate.getPrivate());
            }

            return (false);
        }

        public void setPrivate(boolean _private)

        throws TOTorrentException {
            if (fixup()) {

                delegate.setPrivate(_private);
            }
        }

        public void setAdditionalStringProperty(String name, String value) {
            if (fixup()) {

                delegate.setAdditionalStringProperty(name, value);
            }
        }

        public String getAdditionalStringProperty(String name) {
            Map c = cache;

            if (c != null && (name.equals("encoding") || name.equals("torrent filename"))) {

                byte[] res = (byte[]) c.get(name);

                if (res == null) {

                    return (null);
                }

                try {
                    return (new String(res, "UTF8"));

                } catch (Throwable e) {

                    Debug.printStackTrace(e);

                    return (null);
                }
            }

            if (fixup()) {

                return (delegate.getAdditionalStringProperty(name));
            }

            return (null);
        }

        public void setAdditionalByteArrayProperty(String name, byte[] value) {
            if (fixup()) {

                delegate.setAdditionalByteArrayProperty(name, value);
            }
        }

        public byte[] getAdditionalByteArrayProperty(String name) {
            if (fixup()) {

                return (delegate.getAdditionalByteArrayProperty(name));
            }

            return (null);
        }

        public void setAdditionalLongProperty(String name, Long value) {
            if (fixup()) {

                delegate.setAdditionalLongProperty(name, value);
            }
        }

        public Long getAdditionalLongProperty(String name) {
            if (fixup()) {

                return (delegate.getAdditionalLongProperty(name));
            }

            return (null);
        }

        public void setAdditionalListProperty(String name, List value) {
            if (fixup()) {

                delegate.setAdditionalListProperty(name, value);
            }
        }

        public List getAdditionalListProperty(String name) {
            if (fixup()) {

                return (delegate.getAdditionalListProperty(name));
            }

            return (null);
        }

        public void setAdditionalMapProperty(String name, Map value) {
            if (fixup()) {

                delegate.setAdditionalMapProperty(name, value);
            }
        }

        public Map getAdditionalMapProperty(String name) {
            Map c = cache_attributes;

            if (c != null && name.equals("attributes")) {

                return (c);
            }

            c = cache_azp;

            if (c != null && name.equals("azureus_properties")) {

                return (c);
            }

            if (fixup()) {

                return (delegate.getAdditionalMapProperty(name));
            }

            return (null);
        }

        public Object getAdditionalProperty(String name) {
            if (fixup()) {

                return (delegate.getAdditionalProperty(name));
            }

            return (null);
        }

        public void setAdditionalProperty(String name, Object value) {
            if (fixup()) {

                delegate.setAdditionalProperty(name, value);
            }
        }

        public void removeAdditionalProperty(String name) {
            if (fixup()) {

                delegate.removeAdditionalProperty(name);
            }
        }

        public void removeAdditionalProperties() {
            if (fixup()) {

                delegate.removeAdditionalProperties();
            }
        }

        public void serialiseToBEncodedFile(File file)

        throws TOTorrentException {
            if (fixup()) {

                delegate.serialiseToBEncodedFile(file);

                return;
            }

            throw (fixup_failure);
        }

        public Map serialiseToMap()

        throws TOTorrentException {
            if (fixup()) {

                return (delegate.serialiseToMap());
            }

            throw (fixup_failure);
        }

        public void serialiseToXMLFile(File file)

        throws TOTorrentException {
            if (fixup()) {

                delegate.serialiseToXMLFile(file);

                return;
            }

            throw (fixup_failure);
        }

        public void addListener(TOTorrentListener l) {
            if (fixup()) {

                delegate.addListener(l);
            }
        }

        public void removeListener(TOTorrentListener l) {
            if (fixup()) {

                delegate.removeListener(l);
            }
        }

        public AEMonitor getMonitor() {
            if (fixup()) {

                return (delegate.getMonitor());
            }

            return (null);
        }

        public void print() {
            if (fixup()) {

                delegate.print();
            }
        }

        /*
         * (non-Javadoc)
         * @see org.gudy.azureus2.core3.logging.LogRelation#getLogRelationText()
         */
        public String getRelationText() {
            return "Torrent: '" + new String(getName()) + "'";
        }

        /*
         * (non-Javadoc)
         * @see org.gudy.azureus2.core3.logging.LogRelation#queryForClass(java.lang.Class)
         */
        public Object[] getQueryableInterfaces() {
            // yuck
            try {
                return new Object[] { AzureusCoreFactory.getSingleton().getGlobalManager().getDownloadManager(this) };
            } catch (Exception e) {
            }

            return null;
        }
    }
}